#!/bin/sh
# Script to set up one of the nodes as a NAT gateway for all other nodes.
# This is used to ensure that all nodes in the cluster can still originate
# traffic to the external network even if there are no public addresses
# available.
#

[ -n "$CTDB_BASE" ] || \
    CTDB_BASE=$(d=$(dirname "$0") ; cd -P "$d" ; dirname "$PWD")

. "${CTDB_BASE}/functions"

# service_name is used by various functions
# shellcheck disable=SC2034
service_name=natgw

loadconfig

[ -n "$CTDB_NATGW_NODES" ] || exit 0
export CTDB_NATGW_NODES

service_state_dir=$(ctdb_setup_service_state_dir) || exit $?

natgw_cfg_new="${service_state_dir}/cfg_new"
natgw_cfg_old="${service_state_dir}/cfg_old"
natgw_master_old="${service_state_dir}/master_old"

ctdb_natgw_slave_only ()
{
    _ip_address=$(ctdb_get_ip_address)

    awk -v my_ip="$_ip_address" \
	'$1 == my_ip { if ($2 ~ "slave-only") { exit 0 } else { exit 1 } }' \
	"$CTDB_NATGW_NODES"
}

natgw_check_config ()
{
    [ -r "$CTDB_NATGW_NODES" ] || \
	die "error: CTDB_NATGW_NODES=${CTDB_NATGW_NODES} unreadable"
    if ! ctdb_natgw_slave_only ; then
	[ -n "$CTDB_NATGW_PUBLIC_IP" ] || \
	    die "Invalid configuration: CTDB_NATGW_PUBLIC_IP not set"
	[ -n "$CTDB_NATGW_PUBLIC_IFACE" ] || \
	    die "Invalid configuration: CTDB_NATGW_PUBLIC_IFACE not set"
    fi
    [ -n "$CTDB_NATGW_PRIVATE_NETWORK" ] || \
	    die "Invalid configuration: CTDB_NATGW_PRIVATE_NETWORK not set"

    if [ "$CTDB_PARTIALLY_ONLINE_INTERFACES" = "yes" ] ; then
	    die "Invalid configuration: CTDB_PARTIALLY_ONLINE_INTERFACES=yes incompatible with NAT gateway"
    fi

    # The default is to create a single default route
    [ -n "$CTDB_NATGW_STATIC_ROUTES" ] || CTDB_NATGW_STATIC_ROUTES="0.0.0.0/0"
}

natgw_write_config ()
{
    _f="$1"

    cat >"$_f" <<EOF
CTDB_NATGW_NODES="$CTDB_NATGW_NODES"
CTDB_NATGW_PUBLIC_IP="$CTDB_NATGW_PUBLIC_IP"
CTDB_NATGW_PUBLIC_IFACE="$CTDB_NATGW_PUBLIC_IFACE"
CTDB_NATGW_DEFAULT_GATEWAY="$CTDB_NATGW_DEFAULT_GATEWAY"
CTDB_NATGW_PRIVATE_NETWORK="$CTDB_NATGW_PRIVATE_NETWORK"
CTDB_NATGW_STATIC_ROUTES="$CTDB_NATGW_STATIC_ROUTES"
EOF
}

natgw_config_has_changed ()
{
    natgw_write_config "$natgw_cfg_new"

    # Non-existent old returns true, no log message
    if [ ! -f "$natgw_cfg_old" ] ; then
	return 0
    fi

    # Handle no change
    if cmp "$natgw_cfg_old" "$natgw_cfg_new" >/dev/null 2>&1 ; then
	return 1
    fi

    echo "NAT gateway configuration has changed"
    return 0
}

_natgw_clear ()
{
    _ip="${CTDB_NATGW_PUBLIC_IP%/*}"
    _maskbits="${CTDB_NATGW_PUBLIC_IP#*/}"

    delete_ip_from_iface \
	"$CTDB_NATGW_PUBLIC_IFACE" "$_ip" "$_maskbits" >/dev/null 2>&1
    for _net_gw in $CTDB_NATGW_STATIC_ROUTES ; do
	_net="${_net_gw%@*}"
	ip route del "$_net" metric 10 >/dev/null 2>/dev/null
    done

    # Delete the masquerading setup from a previous iteration where we
    # were the NAT-GW
    iptables -D POSTROUTING -t nat \
	-s "$CTDB_NATGW_PRIVATE_NETWORK" ! -d "$CTDB_NATGW_PRIVATE_NETWORK" \
	-j MASQUERADE >/dev/null 2>/dev/null

    iptables -D INPUT -p tcp --syn -d "${_ip}/32" -j REJECT 2>/dev/null
}

natgw_clear ()
{
    if [ -r "$natgw_cfg_old" ] ; then
	(. "$natgw_cfg_old" ; _natgw_clear)
    else
	_natgw_clear
    fi
}

natgw_set_master ()
{
    set_proc sys/net/ipv4/ip_forward 1
    iptables -A POSTROUTING -t nat \
	-s "$CTDB_NATGW_PRIVATE_NETWORK" ! -d "$CTDB_NATGW_PRIVATE_NETWORK" \
	-j MASQUERADE

    # block all incoming connections to the NATGW IP address
    ctdb_natgw_public_ip_host="${CTDB_NATGW_PUBLIC_IP%/*}/32"
    iptables -D INPUT -p tcp --syn \
	-d "$ctdb_natgw_public_ip_host" -j REJECT 2>/dev/null
    iptables -I INPUT -p tcp --syn \
	-d "$ctdb_natgw_public_ip_host" -j REJECT 2>/dev/null

    ip addr add "$CTDB_NATGW_PUBLIC_IP" dev "$CTDB_NATGW_PUBLIC_IFACE"
    for _net_gw in $CTDB_NATGW_STATIC_ROUTES ; do
	_net="${_net_gw%@*}"
	if [ "$_net" != "$_net_gw" ] ; then
	    _gw="${_net_gw#*@}"
	else
	    _gw="$CTDB_NATGW_DEFAULT_GATEWAY"
	fi

	[ -n "$_gw" ] || continue
	ip route add "$_net" metric 10 via "$_gw"
    done
}

natgw_set_slave ()
{
    _natgwip="$1"

    for _net_gw in $CTDB_NATGW_STATIC_ROUTES ; do
	_net="${_net_gw%@*}"
	ip route add "$_net" via "$_natgwip" metric 10
    done
}

natgw_ensure_master ()
{
    # Intentional word splitting here
    # shellcheck disable=SC2046
    set -- $("${CTDB_HELPER_BINDIR}/ctdb_natgw" master)
    natgwmaster="${1:--1}" # Default is -1, for failure above
    natgwip="$2"

    if [ "$natgwmaster" = "-1" ]; then
	# Fail...
	die "There is no NATGW master node"
    fi
}

natgw_master_has_changed ()
{
    if [ -r "$natgw_master_old" ] ; then
	read _old_natgwmaster <"$natgw_master_old"
    else
	_old_natgwmaster=""
    fi
    [ "$_old_natgwmaster" != "$natgwmaster" ]
}

natgw_save_state ()
{
    echo "$natgwmaster" >"$natgw_master_old"
    # Created by natgw_config_has_changed()
    mv "$natgw_cfg_new" "$natgw_cfg_old"
}


case "$1" in
setup)
	natgw_check_config
	;;

startup)
	natgw_check_config

	# Error if CTDB_NATGW_PUBLIC_IP is listed in public addresses
	ip_pat=$(echo "$CTDB_NATGW_PUBLIC_IP" | sed -e 's@\.@\\.@g')
	if grep -q "^${ip_pat}[[:space:]]" \
	    "${CTDB_PUBLIC_ADDRESSES:-${CTDB_BASE}/public_addresses}" ; then
	    die "ERROR: CTDB_NATGW_PUBLIC_IP same as a public address"
	fi

	# do not send out arp requests from loopback addresses
	set_proc sys/net/ipv4/conf/all/arp_announce 2
	;;

updatenatgw|ipreallocated)
	natgw_check_config

	natgw_ensure_master

	natgw_config_has_changed || natgw_master_has_changed || exit 0

	natgw_clear

	pnn=$(ctdb_get_pnn)
	if [ "$pnn" = "$natgwmaster" ]; then
	    natgw_set_master
	else
	    natgw_set_slave "$natgwip"
	fi

	# flush our route cache
	set_proc sys/net/ipv4/route/flush 1

	# Only update saved state when NATGW successfully updated
	natgw_save_state
	;;

shutdown|removenatgw)
	natgw_check_config
	natgw_clear
	;;

monitor)
	natgw_check_config

	if [ -n "$CTDB_NATGW_PUBLIC_IFACE" ] ; then
	    interface_monitor "$CTDB_NATGW_PUBLIC_IFACE" || exit 1
	fi
	;;
esac

exit 0
